/**
 * This file is part of decent-cpp.
 *
 * decent-cpp is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * decent-cpp is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with decent-cpp.  If not, see <http://www.gnu.org/licenses/>.
 */

#define DC_DATA_EXPORT_PERMS
#include "data.h"
#include <sqlite3.h>
#include <stdio.h>
#include <algorithm>
#include <exception>
#include <regex>
#include <string>
#include <vector>
#include "util.h"

namespace dc {
/// Bind an int to an sqlite statement.
void sqlite_stmt_bind(sqlite3_stmt* st, int position, int value) {
  int rc = sqlite3_bind_int(st, position, value);
  if (rc != SQLITE_OK) {
    std::string msg("Failed to apply arg: ");
    msg += sqlite3_errstr(rc);
    throw std::runtime_error(msg);
  }
}

/// Bind a 64 bit int to an sqlite statement.
void sqlite_stmt_bind(sqlite3_stmt* st, int position, u64_t value) {
  int rc = sqlite3_bind_int64(st, position, value);
  if (rc != SQLITE_OK) {
    std::string msg("Failed to apply arg: ");
    msg += sqlite3_errstr(rc);
    throw std::runtime_error(msg);
  }
}

/// Bind a string to an sqlite statement.
void sqlite_stmt_bind(sqlite3_stmt* st, int position,
                      std::string const& value) {
  int rc =
      sqlite3_bind_text(st, position, value.c_str(), value.size(), nullptr);
  if (rc != SQLITE_OK) {
    std::string msg("Failed to apply arg: ");
    msg += sqlite3_errstr(rc);
    throw std::runtime_error(msg);
  }
}

/// Bind bytes to an sqlite statement.
template <size_t L>
void sqlite_stmt_bind(sqlite3_stmt* st, int position,
                      std::array<u8_t, L> const& value) {
  int rc = sqlite3_bind_blob(st, position, value.data(), value.size(), nullptr);
  if (rc != SQLITE_OK) {
    std::string msg("Failed to apply arg: ");
    msg += sqlite3_errstr(rc);
    throw std::runtime_error(msg);
  }
}

class SqliteStatement;

/// Sqlite result proxy.
class SqliteValue {
  friend class SqliteStatement;

 private:
  sqlite3_stmt* stmt;
  int idx;
  SqliteValue(sqlite3_stmt* stmt, int idx) : stmt(stmt), idx(idx) {}

 public:
  /// Gets a string at column idx.
  operator std::string() const {
    const unsigned char* text = sqlite3_column_text(stmt, idx);
    if (text == nullptr) {
      throw std::runtime_error("SQLite fetch error");
    }
    return std::string(reinterpret_cast<const char*>(text));
  };

  /// Gets an integer at column idx.
  operator int() const { return sqlite3_column_int(stmt, idx); };

  /// Gets a boolean at column idx.
  operator bool() const { return (int)*this != 0; }

  /// Gets a 64 bit int at column idx.
  operator u64_t() const { return sqlite3_column_int64(stmt, idx); }

  /// Gets an array type at column idx.
  template <size_t L>
  operator std::array<u8_t, L>() const {
    int len = sqlite3_column_bytes(stmt, idx);
    if (len != L) {
      throw std::runtime_error("Length of blob does not match");
    }
    const void* bytes = sqlite3_column_blob(stmt, idx);
    std::array<u8_t, L> arr;
    memcpy(arr.data(), bytes, len);
    return arr;
  }
};

/// RAII for sqlite mutex
class SqliteGuard {
 private:
  sqlite3* db;

 public:
  SqliteGuard(sqlite3* db) : db(db) {
    sqlite3_mutex_enter(sqlite3_db_mutex(db));
  }
  ~SqliteGuard() { sqlite3_mutex_leave(sqlite3_db_mutex(db)); }
};

/// Sqlite prepared statement wrapper with exceptions and RAII.
class SqliteStatement {
 private:
  sqlite3_stmt* statement;
  bool reading;
  bool done;

 public:
  SqliteStatement(SqliteStatement& other) = delete;
  SqliteStatement(SqliteStatement&& other) = delete;
  SqliteStatement& operator=(SqliteStatement&& other) = delete;
  template <typename... Ts>
  SqliteStatement(sqlite3* db, std::string const& query, Ts const&... args)
      : statement(nullptr), reading(false), done(false) {
    int rc = sqlite3_prepare_v2(db, query.c_str(), query.size(), &statement,
                                nullptr);
    if (rc != SQLITE_OK) {
      throw std::runtime_error("Failed to prepare statement");
    }
    int pos = 1;
    (void)(int[]){(sqlite_stmt_bind(statement, pos++, args), 0)...};
  }

  /// Loops through all rows.
  void finish() {
    while (++*this) {
    }
  }

  /// Gets the next row. Returns true if we got a row, false if we're done, or
  /// throws on error.
  bool operator++() {
    if (done) {
      throw std::runtime_error(
          "Invalid state: SQLite already reported done or error");
    }
    int rc;
    do {
      rc = sqlite3_step(statement);
    } while (rc == SQLITE_BUSY);
    if (rc == SQLITE_ROW) {
      reading = true;
      return true;
    }
    if (rc == SQLITE_DONE) {
      reading = false;
      done = true;
      return false;
    }
    reading = false;
    done = true;
    std::string message = "SQLite error: ";
    message += rc;
    throw std::runtime_error(message);
  }

  /// Retrieves the data at column idx.
  SqliteValue operator[](int idx) const {
    if (!reading) {
      throw std::runtime_error("Statement is not reading");
    }
    if (done) {
      throw std::runtime_error("Statement is done");
    }
    return SqliteValue(statement, idx);
  };

  ~SqliteStatement() {
    if (statement != nullptr) {
      sqlite3_finalize(statement);
    }
  }
};

/// Main db handle.
sqlite3* db;
/// Dummy password and hash
std::string dummy_pwd = "megathink";
std::string dummy_hash;  // initialized in data_init()

// Permission
const std::array<std::string, static_cast<size_t>(Permission::SIZE)>
    permissionStrings{{
#define DC_PERM(x) std::string(#x)
        DC_PERMS
#undef DC_PERM
    }};
std::string const& permissionToString(Permission perm) {
  if (perm >= Permission::SIZE) {
    throw std::invalid_argument("Gib real permission pls");
  }
  return permissionStrings[static_cast<size_t>(perm)];
}
Permission permissionFromInt(int val) {
  if (val >= static_cast<int>(Permission::SIZE)) {
    throw std::invalid_argument("Bad permission value");
  }
  return static_cast<Permission>(val);
}
nonstd::optional<Permission> permissionFromString(std::string const& str) {
  auto it = std::find(permissionStrings.begin(), permissionStrings.end(), str);
  if (it != permissionStrings.end()) {
    auto index = std::distance(permissionStrings.begin(), it);
    return permissionFromInt(index);
  } else {
    return nonstd::nullopt;
  }
}
permissions_t makePerms(std::vector<Permission> yes,
                        std::vector<Permission> no) {
  permissions_t bitstr;
  for (auto p : yes) {
    permissionsSet(bitstr, p, DC_PERM_YES);
  }
  for (auto p : no) {
    permissionsSet(bitstr, p, DC_PERM_NO);
  }
  return bitstr;
}

unsigned permissionsGet(permissions_t const& perms, Permission perm) {
  unsigned x = 0;
  size_t p = static_cast<size_t>(perm);
  x |= perms.test(p * 2);
  x |= perms.test(p * 2 + 1) << 1;
  return x & 0b11u;
}

void permissionsSet(permissions_t& perms, Permission perm, unsigned val) {
  size_t p = static_cast<size_t>(perm);
  if (val == DC_PERM_NEUTRAL) {
    perms.reset(p * 2);
    perms.reset(p * 2 + 1);
  } else if (val == DC_PERM_NO) {
    perms.set(p * 2);
    perms.reset(p * 2 + 1);
  } else if (val == DC_PERM_YES) {
    perms.reset(p * 2);
    perms.set(p * 2 + 1);
  } else {
    throw std::invalid_argument("Bad val to permissionsSet");
  }
}

nlohmann::json permissionsToJson(permissions_t perms) {
  nlohmann::json obj(nlohmann::json::object());
  for (size_t i = 0; i < static_cast<size_t>(Permission::SIZE); i++) {
    unsigned val = permissionsGet(perms, static_cast<Permission>(i));
    if (val == DC_PERM_YES) {
      obj[permissionToString(static_cast<Permission>(i))] = true;
    } else if (val == DC_PERM_NO) {
      obj[permissionToString(static_cast<Permission>(i))] = false;
    }
  }
  return obj;
}

// Role
const std::string Role::EVERYONE("_everyone");
const std::string Role::USER("_user");
const std::string Role::GUEST("_guest");
const permissions_t default_perms =
    makePerms({Permission::readMessages, Permission::sendMessages,
               Permission::uploadImages},
              {});
void Role::initTable() {
  {
    SqliteStatement stmt(db,
                         "CREATE TABLE IF NOT EXISTS `roles` ("
                         "`id` INTEGER PRIMARY KEY,"
                         "`name` TEXT UNIQUE NOT NULL,"
                         "`permissions` INTEGER NOT NULL,"
                         "`default` INTEGER NOT NULL);");
    stmt.finish();
  }
  // Check for everyone role
  bool hasEveryone = false;
  {
    SqliteStatement stmt(db, "SELECT * FROM `roles` WHERE `name`=?", EVERYONE);
    if (++stmt) {
      hasEveryone = true;
    }
  }
  if (!hasEveryone) {
    SqliteStatement stmt(db,
                         "INSERT INTO `roles`"
                         "(`name`, `permissions`, `default`)"
                         "VALUES (?, ?, ?)",
                         EVERYONE, default_perms.to_ullong(), false);
    stmt.finish();
  }
  bool hasUser = false;
  {
    SqliteStatement stmt(db, "SELECT * FROM `roles` WHERE `name`=?", USER);
    if (++stmt) {
      hasUser = true;
    }
  }
  if (!hasUser) {
    SqliteStatement stmt(db,
                         "INSERT INTO `roles`"
                         "(`name`, `permissions`, `default`)"
                         "VALUES (?, ?, ?)",
                         USER, default_perms.to_ullong(), false);
    stmt.finish();
  }
  bool hasGuest = false;
  {
    SqliteStatement stmt(db, "SELECT * FROM `roles` WHERE `name`=?", GUEST);
    if (++stmt) {
      hasGuest = true;
    }
  }
  if (!hasGuest) {
    SqliteStatement stmt(db,
                         "INSERT INTO `roles`"
                         "(`name`, `permissions`, `default`)"
                         "VALUES (?, ?, ?)",
                         GUEST, default_perms.to_ullong(), false);
    stmt.finish();
  }
}

Role::Role(int id, std::string name, permissions_t permissions, bool isDefault)
    : id(id), name(name), permissions(permissions), isDefault(isDefault) {}

Role::Role(const Role& other)
    : id(other.id),
      name(other.name),
      permissions(other.permissions),
      isDefault(other.isDefault) {}

nlohmann::json Role::to_json() const {
  nlohmann::json obj;
  obj["id"] = std::to_string(id);
  obj["name"] = name;
  obj["permissions"] = permissionsToJson(permissions);
  obj["default"] = isDefault;
  return obj;
}

permissions_t Role::getPermissions() { return permissions; }
std::string Role::getName() {
  if (name == EVERYONE || name == GUEST || name == USER) {
    return name;
  }
  return std::to_string(id);
}
int Role::getId() { return id; }

/// Builds a role from the given stmt, given there is a row
Role roleFromStmt(SqliteStatement& stmt) {
  int id = stmt[0];
  std::string name = stmt[1];
  u64_t permissionsLong = stmt[2];
  permissions_t permissions(permissionsLong);
  bool isDefault = stmt[3];
  return Role(id, name, permissions, isDefault);
}

std::vector<Role> Role::getAllRoles() {
  std::vector<Role> roles;
  SqliteStatement stmt(db, "SELECT * FROM `roles`");
  while (++stmt) {
    roles.emplace_back(roleFromStmt(stmt));
  }
  return roles;
}

nonstd::optional<Role> Role::getRole(const std::string& name) {
  if (name == EVERYONE || name == GUEST || name == USER) {
    SqliteStatement stmt(db, "SELECT * FROM `roles` WHERE `name`=?", name);
    if (++stmt) {
      return roleFromStmt(stmt);
    } else {
      return nonstd::nullopt;
    }
  } else {
    int roleId;
    try {
      roleId = std::stoi(name);
    } catch (...) {
      return nonstd::nullopt;
    }
    return getRole(roleId);
  }
}

nonstd::optional<Role> Role::getRole(int id) {
  SqliteStatement stmt(db, "SELECT * FROM `roles` WHERE `id`=?", id);
  if (++stmt) {
    return roleFromStmt(stmt);
  } else {
    return nonstd::nullopt;
  }
}

nonstd::optional<Role> Role::newRole(std::string name,
                                     permissions_t permissions,
                                     bool isDefault) {
  SqliteGuard guard(db);
  SqliteStatement insert(db,
                         "INSERT OR IGNORE INTO `roles` "
                         "(`name`, `permissions`, `default`) "
                         "VALUES (?, ?, ?)",
                         name, permissions.to_ullong(), isDefault);
  insert.finish();
  int id = sqlite3_last_insert_rowid(db);
  int success = sqlite3_changes(db);
  // printf("Got id %d, success %d\n", id, success);
  if (!success) {
    return nonstd::nullopt;
  }
  return Role(id, name, permissions, isDefault);
}

bool Role::edit(nonstd::optional<std::string> name,
                nonstd::optional<permissions_t> perms,
                nonstd::optional<bool> isDefault) {
  std::string newName = this->name;
  permissions_t newPerms = this->permissions;
  bool newIsDefault = this->isDefault;
  bool isSpecial = this->name == Role::EVERYONE || this->name == Role::GUEST ||
                   this->name == Role::USER;
  if (name.has_value() && !isSpecial) {
    newName = *name;
  }
  if (perms.has_value()) {
    newPerms = *perms;
  }
  if (isDefault.has_value()) {
    newIsDefault = *isDefault;
  }
  SqliteGuard guard(db);
  SqliteStatement edit(
      db,
      "UPDATE OR IGNORE `roles` "
      "SET `name`=?, `permissions`=?, `default`=? WHERE `id`=?",
      newName, newPerms.to_ullong(), newIsDefault, this->id);
  edit.finish();

  if (sqlite3_changes(db) == 0) {
    return false;
  }

  this->name = newName;
  this->permissions = newPerms;
  this->isDefault = newIsDefault;
  return true;
}

void Role::delet() {
  bool isSpecial = this->name == Role::EVERYONE || this->name == Role::GUEST ||
                   this->name == Role::USER;
  if (isSpecial) {
    return;
  }
  SqliteStatement delet(db, "DELETE FROM `roles` WHERE `id`=?", id);
  delet.finish();
}

// User
void User::initTable() {
  {
    SqliteStatement usersTable(db,
                               "CREATE TABLE IF NOT EXISTS `users` ("
                               "`id` INTEGER PRIMARY KEY,"
                               "`username` TEXT NOT NULL UNIQUE,"
                               "`avatarURL` TEXT NOT NULL,"
                               "`flair` TEXT NOT NULL,"
                               "`online` INTEGER NOT NULL,"
                               "`email` TEXT NOT NULL,"
                               "`password` TEXT NOT NULL);");
    usersTable.finish();
  }
  {
    SqliteStatement rolesTable(
        db,
        "CREATE TABLE IF NOT EXISTS `userRoles` ("
        "`userId` INTEGER NOT NULL,"
        "`roleId` INTEGER NOT NULL,"
        "FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE, "
        "FOREIGN KEY (`roleId`) REFERENCES `roles`(`id`) ON DELETE CASCADE);");
    rolesTable.finish();
  }
}

User::User(int id, std::string username, std::string avatarUrl,
           std::string flair, bool online, std::string email)
    : id(id),
      username(username),

      avatarUrl(avatarUrl),
      flair(flair),
      online(online),

      email(email) {}

User::User(const User& other)
    : id(other.id),
      username(other.username),

      avatarUrl(other.avatarUrl),
      flair(other.flair),
      online(other.online),

      email(other.email) {}

nlohmann::json User::to_json(bool isSelf) const {
  nlohmann::json user;
  user["id"] = std::to_string(id);
  user["username"] = username;
  user["avatarURL"] = avatarUrl;
  if (flair.size() == 0) {
    user["flair"] = nullptr;
  } else {
    user["flair"] = flair;
  }
  user["online"] = online;
  user["roleIDs"] = nlohmann::json::array();
  std::vector<Role> roles = getRoles();
  int i = 0;
  for (Role& role : roles) {
    user["roleIDs"][i++] = std::to_string(role.getId());
  }
  if (isSelf) {
    if (email.size() == 0) {
      user["email"] = nullptr;
    } else {
      user["email"] = email;
    }
  }
  return user;
}

int User::getId() const { return id; }

void User::setOnline(bool online) {
  SqliteStatement stmt(db, "UPDATE `users` SET `online`=? WHERE `id`=?", online,
                       id);
  stmt.finish();
  this->online = online;
}

std::vector<Role> User::getRoles() const {
  std::vector<Role> roles;
  SqliteStatement stmt(
      db,
      "SELECT `roles`.* FROM `userRoles` "
      "INNER JOIN `roles` ON `userRoles`.`roleId`=`roles`.`id` "
      "OR `roles`.`default`=1 "
      "WHERE `userId`=?",
      id);
  while (++stmt) {
    roles.emplace_back(roleFromStmt(stmt));
  }
  return roles;
}

void applyPermLowerPriority(permissions_t& effective, permissions_t& apply) {
  for (size_t i = 0; i < static_cast<size_t>(Permission::SIZE); i++) {
    Permission ip = static_cast<Permission>(i);
    unsigned applyPerm = permissionsGet(apply, ip);
    unsigned existingPerm = permissionsGet(effective, ip);
    // printf("Masking perm %s, role=%d, existing=%d\n",
    //       permissionToString(ip).c_str(), applyPerm, existingPerm);
    if (existingPerm == DC_PERM_NEUTRAL && applyPerm != DC_PERM_NEUTRAL) {
      permissionsSet(effective, ip, applyPerm);
    }
  }
}

permissions_t User::effectivePermissions(nonstd::optional<Channel> channel) {
  // printf("Calculating effective perms\n");
  permissions_t effective;

  Role everyone = *Role::getRole(Role::EVERYONE);
  Role user = *Role::getRole(Role::USER);

  std::vector<Role> roles = getRoles();
  // Channel privileges
  if (channel.has_value()) {
    permissions_t highestChannelPerms;
    for (Role& role : roles) {
      permissions_t rolePerms = channel->getRolePermissions(role);

      for (size_t i = 0; i < static_cast<size_t>(Permission::SIZE); i++) {
        Permission ip = static_cast<Permission>(i);
        unsigned rolePerm = permissionsGet(rolePerms, ip);
        unsigned existingPerm = permissionsGet(highestChannelPerms, ip);

        if (rolePerm == DC_PERM_YES) {
          permissionsSet(highestChannelPerms, ip, DC_PERM_YES);
        } else if (rolePerm == DC_PERM_NO && existingPerm == DC_PERM_NEUTRAL) {
          permissionsSet(highestChannelPerms, ip, DC_PERM_NO);
        }
      }
    }
    applyPermLowerPriority(effective, highestChannelPerms);
    permissions_t channelUser = channel->getRolePermissions(user);
    applyPermLowerPriority(effective, channelUser);
    permissions_t channelEveryone = channel->getRolePermissions(everyone);
    applyPermLowerPriority(effective, channelEveryone);
  }

  // Calculate highest privileges allowed by the combination of roles
  permissions_t highestRolePerms;
  for (Role& role : roles) {
    // printf("Calculating perm for %s\n", role.getName().c_str());
    permissions_t rolePerms = role.getPermissions();

    for (size_t i = 0; i < static_cast<size_t>(Permission::SIZE); i++) {
      Permission ip = static_cast<Permission>(i);
      unsigned rolePerm = permissionsGet(rolePerms, ip);
      unsigned existingPerm = permissionsGet(highestRolePerms, ip);

      // printf("Masking perm %s, role=%d, existing=%d\n",
      //       permissionToString(ip).c_str(), rolePerm, existingPerm);

      if (rolePerm == DC_PERM_YES) {
        permissionsSet(highestRolePerms, ip, DC_PERM_YES);
      } else if (rolePerm == DC_PERM_NO && existingPerm == DC_PERM_NEUTRAL) {
        permissionsSet(highestRolePerms, ip, DC_PERM_NO);
      }
    }
  }
  // Apply role privileges to effective
  // printf("Masking role perms\n");
  applyPermLowerPriority(effective, highestRolePerms);
  // printf("Masking user perms\n");
  permissions_t globalUser = user.getPermissions();
  applyPermLowerPriority(effective, globalUser);
  // printf("Masking everyone perms\n");
  permissions_t globalEveryone = everyone.getPermissions();
  applyPermLowerPriority(effective, globalEveryone);
  // printf("Masking no over neutral\n");
  for (size_t i = 0; i < static_cast<size_t>(Permission::SIZE); i++) {
    Permission ip = static_cast<Permission>(i);
    if (permissionsGet(effective, ip) == DC_PERM_NEUTRAL) {
      // printf("Masking %s as no\n", permissionToString(ip).c_str());
      permissionsSet(effective, ip, DC_PERM_NO);
    }
  }
  return effective;
}

// Role again yay
permissions_t Role::effectiveGuestPermissions(
    nonstd::optional<Channel> channel) {
  Role guest = *Role::getRole(Role::GUEST);
  Role everyone = *Role::getRole(Role::EVERYONE);
  permissions_t effective;
  if (channel.has_value()) {
    permissions_t channelGuest = channel->getRolePermissions(guest);
    applyPermLowerPriority(effective, channelGuest);
    permissions_t channelEveryone = channel->getRolePermissions(everyone);
    applyPermLowerPriority(effective, channelEveryone);
  }
  permissions_t globalGuest = guest.getPermissions();
  applyPermLowerPriority(effective, globalGuest);
  permissions_t globalEveryone = everyone.getPermissions();
  applyPermLowerPriority(effective, globalEveryone);
  for (size_t i = 0; i < static_cast<size_t>(Permission::SIZE); i++) {
    Permission ip = static_cast<Permission>(i);
    if (permissionsGet(effective, ip) == DC_PERM_NEUTRAL) {
      permissionsSet(effective, ip, DC_PERM_NO);
    }
  }
  return effective;
}

/// Fetches a user from a statement, given the statement currently has a row
User userFromStmt(SqliteStatement& stmt) {
  int id = stmt[0];
  std::string username = stmt[1];
  std::string avatarUrl = stmt[2];
  std::string flair = stmt[3];
  bool online = stmt[4];
  std::string email = stmt[5];
  return User(id, username, avatarUrl, flair, online, email);
}

std::vector<User> User::getAllUsers() {
  SqliteStatement stmt(db, "SELECT * FROM `users`");
  std::vector<User> users;
  while (++stmt) {
    users.emplace_back(userFromStmt(stmt));
  }
  return users;
}

int User::loginUser(std::string username, std::string pwd) {
  SqliteStatement stmt(db,
                       "SELECT `password`, `id` FROM `users`"
                       "WHERE `username`=?",
                       username);
  if (++stmt) {
    std::string hash = stmt[0];
    if (dc::pwd_verify(pwd, hash)) {
      int user = stmt[1];
      return user;
    }
  } else {
    // Hash a dummy password to prevent timing attacks. argon2 is constant time
    dc::pwd_verify(dummy_pwd, dummy_hash);
  }
  return -1;
}

nonstd::optional<User> User::registerUser(const std::string& username,
                                          const std::string& pwd) {
  if (pwd.size() < 6) {
    return nonstd::nullopt;
  }
  std::string hash = pwd_hash(pwd);
  SqliteGuard guard(db);
  SqliteStatement stmt(
      db,
      "INSERT OR IGNORE INTO `users` "
      "(`username`, `avatarUrl`, `flair`, `online`, `email`, `password`) "
      "VALUES (?, \"\", \"\", 0, \"\", ?)",
      username, hash);
  stmt.finish();
  int success = sqlite3_changes(db);
  int rowid = sqlite3_last_insert_rowid(db);
  if (success == 0) {
    return nonstd::nullopt;
  } else {
    return User(rowid, username, "", "", 0, "");
  }
}

nonstd::optional<User> User::getUser(int id) {
  SqliteStatement stmt(db, "SELECT * FROM `users` WHERE `id`=?", id);
  if (++stmt) {
    return userFromStmt(stmt);
  }
  return nonstd::nullopt;
}

// Channel
void Channel::initTable() {
  SqliteStatement stmt(db,
                       "CREATE TABLE IF NOT EXISTS `channels` (`id` INTEGER "
                       "PRIMARY KEY, `name` TEXT NOT NULL);");
  stmt.finish();
  SqliteStatement cr(db,
                     "CREATE TABLE IF NOT EXISTS `channelRoles` (`channelId` "
                     "INTEGER NOT NULL, `roleId` INTEGER NOT NULL, "
                     "`permissions` INTEGER NOT NULL, FOREIGN KEY(`channelId`) "
                     "REFERENCES `channels`(`id`) ON DELETE CASCADE, FOREIGN "
                     "KEY(`roleId`) REFERENCES `roles`(`id`) ON DELETE "
                     "CASCADE);");
  cr.finish();
}

Channel::Channel(int id, std::string name) : id(id), name(name) {}
Channel::Channel(const Channel& other) : id(other.id), name(other.name) {}

nlohmann::json Channel::to_json(nonstd::optional<User> user) {
  nlohmann::json obj;
  obj["id"] = std::to_string(id);
  obj["name"] = name;
  if (user.has_value()) {
    SqliteGuard guard(db);
    SqliteStatement unreadFetch(
        db, "SELECT * FROM `unreads` WHERE `userId`=? AND `channelId`=?",
        user->getId(), id);
    if (++unreadFetch) {
      u64_t date = unreadFetch[2];
      SqliteStatement data(db,
                           "SELECT COUNT(*), MIN(`id`) FROM `messages` WHERE "
                           "`channelId`=? AND `dateCreated`>?",
                           id, date);
      if (++data) {
        int count = data[0];
        int minId = data[1];
        obj["unreadMessageCount"] = count;
        obj["oldestUnreadMessageID"] = minId;
      } else {
        obj["unreadMessageCount"] = 0;
        obj["oldestUnreadMessageID"] = nullptr;
      }
    } else {
      obj["unreadMessageCount"] = 0;
      obj["oldestUnreadMessageID"] = nullptr;
    }
  }
  return obj;
}

/// Converts a statement result into a channel
Channel channelFromStmt(SqliteStatement& stmt) {
  int id = stmt[0];
  std::string name = stmt[1];
  return Channel(id, name);
}

nonstd::optional<Channel> Channel::getChannel(int id) {
  SqliteStatement stmt(db, "SELECT * FROM `channels` WHERE `id`=?", id);
  if (++stmt) {
    return channelFromStmt(stmt);
  }
  return nonstd::nullopt;
}

std::vector<Channel> Channel::getAllChannels() {
  std::vector<Channel> channels;
  SqliteStatement stmt(db, "SELECT * FROM `channels`");
  while (++stmt) {
    channels.emplace_back(channelFromStmt(stmt));
  }
  return channels;
}

bool Channel::edit(std::string name) {
  SqliteGuard guard(db);
  SqliteStatement stmt(
      db, "UPDATE OR IGNORE `channels` SET `name`=? WHERE `id`=?", name, id);
  stmt.finish();
  bool success = sqlite3_changes(db) != 0;
  if (success) {
    this->name = name;
  }
  return success;
}

void Channel::delet() {
  SqliteStatement stmt(db, "DELETE FROM `channels` WHERE `id`=?", id);
  stmt.finish();
}

std::unordered_map<int, permissions_t> Channel::getRolePermissions() {
  std::unordered_map<int, permissions_t> map;
  SqliteStatement stmt(db, "SELECT * FROM `channelRoles` WHERE `channelId`=?",
                       id);
  while (++stmt) {
    int roleId = stmt[1];
    u64_t permsLong = stmt[2];
    map.emplace(roleId, permissions_t(permsLong));
  }
  return map;
}

permissions_t Channel::getRolePermissions(Role role) {
  permissions_t perms;
  SqliteStatement stmt(db,
                       "SELECT `permissions` FROM `channelRoles` WHERE "
                       "`channelId`=? AND `roleId`=?",
                       id, role.getId());
  if (++stmt) {
    perms = permissions_t(stmt[0]);
  }
  return perms;
}

void Channel::setRolePermissions(
    std::unordered_map<int, permissions_t> permissions) {
  for (auto it = permissions.begin(); it != permissions.end(); ++it) {
    int roleId = it->first;
    permissions_t perms = it->second;
    SqliteStatement stmt(db,
                         "INSERT OR REPLACE INTO `channelRoles` (`channelId`, "
                         "`roleId`, `permissions`) VALUES (?, ?, ?);",
                         id, roleId, perms.to_ullong());
    stmt.finish();
  }
}

void Channel::markRead(User user) {
  int userId = user.getId();
  u64_t date = timestamp();
  SqliteStatement mr(
      db,
      "INSERT OR REPLACE INTO `unreads` (`userId`, `channelId`, `date`) "
      "VALUES (?, ?, ?)",
      userId, id, date);
  mr.finish();
}

int Channel::newChannel(std::string name) {
  SqliteGuard guard(db);
  SqliteStatement stmt(
      db, "INSERT OR IGNORE INTO `channels` (`name`) VALUES (?)", name);
  stmt.finish();
  int success = sqlite3_changes(db);
  int channelId = sqlite3_last_insert_rowid(db);
  if (success != 0) {
    return channelId;
  }
  return -1;
}

// Message
void Message::initTable() {
  SqliteStatement messagesTable(
      db,
      "CREATE TABLE IF NOT EXISTS `messages` ("
      "`id` INTEGER PRIMARY KEY,"
      "`channelId` INTEGER NOT NULL,"
      "`type` INTEGER NOT NULL,"
      "`text` TEXT NOT NULL,"
      "`authorId` INTEGER NOT NULL,"
      "`authorUsername` TEXT NOT NULL,"
      "`authorAvatarUrl` TEXT NOT NULL,"
      "`dateCreated` INTEGER NOT NULL,"
      "`dateEdited` INTEGER NOT NULL,"
      "`pinned` INTEGER NOT NULL,"
      "FOREIGN KEY (`channelId`) REFERENCES `channels`(`id`) ON DELETE CASCADE,"
      "FOREIGN KEY (`authorId`) REFERENCES `users`(`id`) ON DELETE CASCADE);");
  messagesTable.finish();

  SqliteStatement unreadTable(
      db,
      "CREATE TABLE IF NOT EXISTS `unreads` ("
      "`userId` INTEGER NOT NULL,"
      "`channelId` INTEGER NOT NULL,"
      "`date` INTEGER NOT NULL,"
      "FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE,"
      "FOREIGN KEY (`channelId`) REFERENCES `channels`(`id`) ON DELETE CASCADE,"
      "UNIQUE (`userId`, `channelId`) ON CONFLICT REPLACE);");
  unreadTable.finish();

  SqliteStatement mentionsTable(
      db,
      "CREATE TABLE IF NOT EXISTS `mentions` ("
      "`messageId` INTEGER NOT NULL,"
      "`userId` INTEGER NOT NULL,"
      "FOREIGN KEY (`messageId`) REFERENCES `messages`(`id`),"
      "FOREIGN KEY (`userId`) REFERENCES `users`(`id`),"
      "UNIQUE (`messageId`, `userId`) ON CONFLICT REPLACE);");
  mentionsTable.finish();
}

Message::Message(int id, int channelId, Type type, std::string text,
                 int authorId, std::string authorUsername,
                 std::string authorAvatarUrl, u64_t dateCreated,
                 u64_t dateEdited, bool pinned)
    : id(id),
      channelId(channelId),
      type(type),
      text(text),
      authorId(authorId),
      authorUsername(authorUsername),
      authorAvatarUrl(authorAvatarUrl),
      dateCreated(dateCreated),
      dateEdited(dateEdited),
      pinned(pinned) {}
Message::Message(const Message& other)
    : id(other.id),
      channelId(other.channelId),
      type(other.type),
      text(other.text),
      authorId(other.authorId),
      authorUsername(other.authorUsername),
      authorAvatarUrl(other.authorAvatarUrl),
      dateCreated(other.dateCreated),
      dateEdited(other.dateEdited),
      pinned(other.pinned) {}

nlohmann::json Message::to_json() {
  nlohmann::json mess;
  mess["id"] = std::to_string(id);
  mess["channelID"] = std::to_string(channelId);
  mess["type"] = typeToString(type);
  mess["text"] = text;
  mess["authorID"] = std::to_string(authorId);
  mess["authorUsername"] = authorUsername;
  mess["authorAvatarURL"] = authorAvatarUrl;
  mess["dateCreated"] = dateCreated;
  if (dateEdited > 0) {
    mess["dateEdited"] = dateEdited;
  } else {
    mess["dateEdited"] = nullptr;
  }
  mess["pinned"] = pinned;
  std::vector<int> mentioned = getMentionedUserIds();
  int i = 0;
  mess["mentionedUserIDs"] = nlohmann::json::array();
  for (int x : mentioned) {
    mess["mentionedUserIDs"][i++] = std::to_string(x);
  }
  return mess;
}

Message::Type intToType(int type) {
  if (type < 0 || type >= static_cast<int>(Message::Type::SIZE)) {
    throw std::runtime_error("Bad int given to intToType");
  }
  return static_cast<Message::Type>(type);
}

int typeToInt(Message::Type type) { return static_cast<int>(type); }

Message messageFromStmt(SqliteStatement& stmt) {
  int id = stmt[0];
  int channelId = stmt[1];
  int typeInt = stmt[2];
  Message::Type type = intToType(typeInt);
  std::string text = stmt[3];
  int authorId = stmt[4];
  std::string authorUsername = stmt[5];
  std::string authorAvatarUrl = stmt[6];
  u64_t dateCreated = stmt[7];
  u64_t dateEdited = stmt[8];
  bool pinned = stmt[9];
  return Message(id, channelId, type, text, authorId, authorUsername,
                 authorAvatarUrl, dateCreated, dateEdited, pinned);
}

int Message::getId() { return id; }

Channel Message::getChannel() { return *Channel::getChannel(channelId); }

std::vector<int> Message::getMentionedUserIds() {
  std::vector<int> mentionedIds;
  SqliteStatement stmt(
      db, "SELECT `userId` FROM `mentions` WHERE `messageId`=?", id);
  while (++stmt) {
    int usr = stmt[0];
    mentionedIds.emplace_back(usr);
  }
  return mentionedIds;
}

std::vector<int> Message::parseMessageForMentions(std::string msg) {
  static std::regex mentionRegex("<@([0-9]+)>");
  std::smatch matcher;
  std::string text = msg;

  std::vector<int> mentions;
  while (std::regex_search(text, matcher, mentionRegex)) {
    int mentionId;
    try {
      mentionId = std::stoi(matcher[1]);
    } catch (...) {
      // ignore, they probably gave us an overflow number
    }

    mentions.emplace_back(mentionId);
    text = matcher.suffix().str();
  }

  return mentions;
}

std::string Message::typeToString(Message::Type type) {
  switch (type) {
    case Message::Type::SYSTEM:
      return "system";
    case Message::Type::USER:
    default:
      return "user";
  }
}

nonstd::optional<Message::Type> Message::stringToType(std::string str) {
  if (str == "system") {
    return Message::Type::SYSTEM;
  } else if (str == "user") {
    return Message::Type::USER;
  } else {
    return nonstd::nullopt;
  }
}

nonstd::optional<Message> Message::getMessage(int id) {
  SqliteStatement stmt(db, "SELECT * FROM `messages` WHERE `id`=?", id);
  if (++stmt) {
    return messageFromStmt(stmt);
  } else {
    return nonstd::nullopt;
  }
}

nonstd::optional<Message> Message::newMessage(Channel channel,
                                              Message::Type type,
                                              std::string text, User author) {
  int channelId = channel.id;
  int authorId = author.id;
  std::string authorUsername = author.username;
  std::string authorAvatarUrl = author.avatarUrl;
  u64_t dateCreated = timestamp();
  int typeInt = typeToInt(type);
  SqliteGuard guard(db);
  SqliteStatement stmt(db,
                       "INSERT OR IGNORE INTO `messages` (`channelId`, `type`, "
                       "`text`, `authorId`, `authorUsername`, "
                       "`authorAvatarUrl`, `dateCreated`, `dateEdited`, "
                       "`pinned`) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0)",
                       channelId, typeInt, text, authorId, authorUsername,
                       authorAvatarUrl, dateCreated);
  stmt.finish();
  int success = sqlite3_changes(db);
  int rowid = sqlite3_last_insert_rowid(db);
  if (success != 0) {
    auto mentions = parseMessageForMentions(text);
    for (auto mention : mentions) {
      SqliteStatement mentionUpdate(db,
                                    "INSERT OR REPLACE INTO `mentions` "
                                    "(`messageId`, `userId`) VALUES (?, ?)",
                                    rowid, mention);
      mentionUpdate.finish();
    }

    return Message(rowid, channelId, type, text, authorId, authorUsername,
                   authorAvatarUrl, dateCreated, 0, false);
  } else {
    return nonstd::nullopt;
  }
}

std::vector<Message> Message::getMessagesInChannel(
    Channel channel, int limit, nonstd::optional<int> beforeId,
    nonstd::optional<int> afterId) {
  int channelId = channel.id;
  if (limit > 50) {
    limit = 50;
  }
  if (limit < 1) {
    limit = 1;
  }
  std::vector<Message> messages;
  if (beforeId.has_value() && !afterId.has_value()) {
    SqliteStatement stmt(db,
                         "SELECT * FROM `messages` WHERE `channelId`=? AND "
                         "`id`<? ORDER BY `id` DESC LIMIT ?",
                         channelId, *beforeId, limit);
    while (++stmt) {
      messages.emplace_back(messageFromStmt(stmt));
    }
  } else if (!beforeId.has_value() && afterId.has_value()) {
    SqliteStatement stmt(db,
                         "SELECT * FROM `messages` WHERE `channelId`=? `id`>? "
                         "ORDER BY `id` DESC LIMIT ?",
                         channelId, *afterId, limit);
    while (++stmt) {
      messages.emplace_back(messageFromStmt(stmt));
    }
  } else if (beforeId.has_value() && afterId.has_value()) {
    SqliteStatement stmt(
        db,
        "SELECT * FROM `messages` WHERE `channelId`=? `id`<? AND `id`>? "
        "ORDER BY `id` DESC LIMIT ?",
        channelId, *beforeId, *afterId, limit);
    while (++stmt) {
      messages.emplace_back(messageFromStmt(stmt));
    }
  } else {
    SqliteStatement stmt(db,
                         "SELECT * FROM `messages` WHERE `channelId`=? ORDER "
                         "BY `id` DESC LIMIT ?",
                         channelId, limit);
    while (++stmt) {
      messages.emplace_back(messageFromStmt(stmt));
    }
  }

  return messages;
}

std::vector<Message> Message::getMentionsForUser(
    User user, int limit, int skip, nonstd::optional<User> requesting) {
  if (limit > 50) {
    limit = 50;
  }
  if (limit < 1) {
    limit = 1;
  }
  if (skip < 0) {
    skip = 0;
  }
  std::vector<Message> messages;
  int numInserted = 0;
  SqliteStatement stmt(
      db,
      "SELECT `messages`.* FROM `mentions` INNER JOIN "
      "`messages` ON `mentions`.`messageId` = `messages`.`id` "
      "WHERE `userId`=? ORDER BY `mentions`.`messageId` DESC",
      user.id);
  for (int i = 0; i < skip; ++i) {
    if (!++stmt) {
      return messages;
    }
  }
  while (++stmt && numInserted < limit) {
    Message msg = messageFromStmt(stmt);
    Channel channel = msg.getChannel();
    permissions_t perms;
    if (requesting.has_value()) {
      perms = requesting->effectivePermissions(channel);
    } else {
      perms = Role::effectiveGuestPermissions(channel);
    }
    if (permissionsGet(perms, Permission::readMessages) == DC_PERM_YES) {
      messages.emplace_back(msg);
      ++numInserted;
    }
  }
  return messages;
}

// Session
void Session::initTable() {
  SqliteStatement sessionsTable(
      db,
      "CREATE TABLE IF NOT EXISTS `sessions` ("
      "`id` BLOB(16) NOT NULL PRIMARY KEY,"
      "`dateCreated` INTEGER NOT NULL,"
      "`user` INTEGER NOT NULL,"
      "FOREIGN KEY(`user`) REFERENCES `users`(`id`));");
  sessionsTable.finish();
}

Session::Session(uuid id, u64_t dateCreated, int user)
    : id(id), dateCreated(dateCreated), user(user) {}

Session::Session(const Session& other)
    : id(other.id), dateCreated(other.dateCreated), user(other.user) {}

nlohmann::json Session::to_json() {
  nlohmann::json sess;
  std::string sid = uuid_to_string(id);
  sess["id"] = sid;
  sess["dateCreated"] = dateCreated;
  return sess;
}

Session Session::newSession(int user) {
  uuid id = dc::random_uuid();
  u64_t date = dc::timestamp();
  SqliteStatement create(db,
                         "INSERT INTO `sessions`"
                         "(`id`, `dateCreated`, `user`)"
                         "VALUES (?, ?, ?)",
                         id, date, user);
  create.finish();
  return Session(id, date, user);
}

/// Fetches a session from a statement, given the statement currently has a row
Session sessionFromStmt(SqliteStatement& stmt) {
  uuid id = stmt[0];
  u64_t date = stmt[1];
  int user = stmt[2];
  return Session(id, date, user);
}

nonstd::optional<Session> Session::getSession(uuid id) {
  SqliteStatement stmt(db,
                       "SELECT * FROM `sessions`"
                       "WHERE `id`=?",
                       id);
  if (++stmt) {
    return sessionFromStmt(stmt);
  }
  return nonstd::nullopt;
}

std::vector<Session> Session::getSessions(int user) {
  std::vector<Session> sessions;
  SqliteStatement stmt(db,
                       "SELECT * FROM `sessions`"
                       "WHERE `user`=?",
                       user);
  while (++stmt) {
    sessions.emplace_back(sessionFromStmt(stmt));
  }
  return sessions;
}

std::vector<Session> Session::getUserSessions() { return getSessions(user); }

void Session::delet() {
  SqliteStatement stmt(db, "DELETE FROM `sessions` WHERE `id`=?", id);
  stmt.finish();
}

User Session::getUser() { return *User::getUser(user); }

uuid Session::getUuid() const { return id; }

// Main functions
void data_init() {
  dummy_hash = pwd_hash(dummy_pwd);

  int rc;

  rc = sqlite3_open_v2(
      DB_FILE, &db,
      SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
      nullptr);

  if (rc) {
    fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
    return;
  }
  printf("Opened database successfully\n");

  SqliteStatement stmt(db,
                       "CREATE TABLE IF NOT EXISTS `settings` ("
                       "`name` varchar(64) NOT NULL PRIMARY KEY,"
                       "`value` TEXT NOT NULL);");
  stmt.finish();

  Role::initTable();
  User::initTable();
  Session::initTable();
  Channel::initTable();
  Message::initTable();
}

void data_close() { sqlite3_close(db); }

nlohmann::json data_setting_all() {
  nlohmann::json settings;
  try {
    settings["name"] = data_setting_get("name", "Unnamed decent-cpp server");
    settings["iconURL"] =
        data_setting_get("iconURL", "https://decent.chat/favicon.ico");
  } catch (std::exception& ex) {
    printf("Caught exception: %s\n", ex.what());
  }
  return settings;
}

nlohmann::json data_setting_get(std::string const& name,
                                nlohmann::json const& defaultVal) {
  // printf("Fetching setting: %s\n", name.c_str());
  std::string query = "SELECT `value` FROM `settings` WHERE `name`=?";
  SqliteStatement stmt(db, query, name);
  if (++stmt) {
    std::string setting = stmt[0];
    return nlohmann::json::parse(setting);
  } else {
    return defaultVal;
  }
}

void data_setting_set(std::string const& name, nlohmann::json const& value) {
  std::string query =
      "INSERT OR REPLACE INTO `settings`\
    (`name`, `value`) VALUES (?, ?)";
  SqliteStatement stmt(db, query, name, value.dump());
  stmt.finish();
}

}  // namespace dc